import {
  isArray,
  isFunction,
  isInvalid,
  isNull,
  isNullOrUndef,
  isNumber,
  isString,
} from 'inferno-shared';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { Readable } from 'stream';
import { renderStyleAttribute } from './prop-renderers';
import {
  createDerivedState,
  escapeText,
  isAttributeNameSafe,
  renderFunctionalComponent,
  voidElements,
} from './utils';
import type { VNode } from 'inferno';
import { mergePendingState } from './stream/streamUtils';

const resolvedPromise = Promise.resolve();

export class RenderStream extends Readable {
  public initNode: any;
  public started: boolean = false;

  constructor(initNode) {
    super();
    this.initNode = initNode;
  }

  public _read(): void {
    if (this.started) {
      return;
    }
    this.started = true;

    resolvedPromise
      .then(() => {
        return this.renderNode(this.initNode, null);
      })
      .then(() => {
        this.push(null);
      })
      .catch((err) => {
        this.emit('error', err);
      });
  }

  public renderNode(vNode, context) {
    const flags = vNode.flags;

    if ((flags & VNodeFlags.Component) > 0) {
      return this.renderComponent(
        vNode,
        context,
        flags & VNodeFlags.ComponentClass,
      );
    }
    if ((flags & VNodeFlags.Element) > 0) {
      return this.renderElement(vNode, context);
    }
    if (isArray(vNode) || (flags & VNodeFlags.Fragment) !== 0) {
      return this.renderArrayOrFragment(vNode, context);
    }

    this.renderText(vNode);
  }

  public renderArrayOrFragment(vNode, context) {
    const childFlags = vNode.childFlags;

    if (
      childFlags === ChildFlags.HasVNodeChildren ||
      (isArray(vNode) && vNode.length === 0)
    ) {
      return this.push('<!--!-->');
    } else if (childFlags & ChildFlags.MultipleChildren || isArray(vNode)) {
      const children = isArray(vNode) ? vNode : vNode.children;

      return (children as VNode[]).reduce(async (p, child) => {
        return await p.then(async () => {
          return await Promise.resolve(this.renderNode(child, context)).then(
            () => !!(child.flags & VNodeFlags.Text),
          );
        });
      }, Promise.resolve(false));
    }
  }

  public renderComponent(vComponent, context, isClass) {
    const type = vComponent.type;
    const props = vComponent.props;

    if (!isClass) {
      const renderOutput = renderFunctionalComponent(vComponent, context);

      if (isInvalid(renderOutput)) {
        return this.push('<!--!-->');
      }
      if (isString(renderOutput)) {
        return this.push(escapeText(renderOutput));
      }
      if (isNumber(renderOutput)) {
        return this.push(renderOutput + '');
      }

      return this.renderNode(renderOutput, context);
    }

    const instance = new type(props, context);
    const hasNewAPI = Boolean(type.getDerivedStateFromProps);
    instance.$BS = false;
    instance.$SSR = true;
    let childContext;
    if (isFunction(instance.getChildContext)) {
      childContext = instance.getChildContext();
    }

    if (!isNullOrUndef(childContext)) {
      context = { ...context, ...childContext };
    }
    instance.context = context;
    instance.$BR = true;

    return Promise.resolve(!hasNewAPI && instance.componentWillMount?.()).then(
      () => {
        mergePendingState(instance);

        if (hasNewAPI) {
          instance.state = createDerivedState(instance, props, instance.state);
        }
        const renderOutput = instance.render(
          instance.props,
          instance.state,
          instance.context,
        );

        if (isInvalid(renderOutput)) {
          return this.push('<!--!-->');
        }
        if (isString(renderOutput)) {
          return this.push(escapeText(renderOutput));
        }
        if (isNumber(renderOutput)) {
          return this.push(renderOutput + '');
        }

        return this.renderNode(renderOutput, context);
      },
    );
  }

  public renderChildren(
    children: VNode[] | VNode | string,
    context: any,
    childFlags: ChildFlags,
  ) {
    if (childFlags === ChildFlags.HasVNodeChildren) {
      return this.renderNode(children, context);
    }
    if (childFlags === ChildFlags.HasTextChildren) {
      return this.push(
        (children as string) === ''
          ? ' '
          : escapeText((children as string) + ''),
      );
    }
    if (childFlags & ChildFlags.MultipleChildren) {
      return (children as VNode[]).reduce(async (p, child) => {
        return await p.then(async () => {
          return await Promise.resolve(this.renderNode(child, context)).then(
            () => !!(child.flags & VNodeFlags.Text),
          );
        });
      }, Promise.resolve(false));
    }
  }

  public renderText(vNode): void {
    this.push(vNode.children === '' ? ' ' : escapeText(vNode.children));
  }

  public renderElement(vNode, context) {
    const type = vNode.type;
    const props = vNode.props;
    let renderedString = `<${type}`;
    let html;
    const isVoidElement = voidElements.has(type);
    const className = vNode.className;

    if (isString(className)) {
      renderedString += ` class="${escapeText(className)}"`;
    } else if (isNumber(className)) {
      renderedString += ` class="${className}"`;
    }

    if (!isNull(props)) {
      for (const prop in props) {
        const value = props[prop];

        switch (prop) {
          case 'dangerouslySetInnerHTML':
            html = value.__html;
            break;
          case 'style':
            if (!isNullOrUndef(props.style)) {
              renderedString += renderStyleAttribute(props.style);
            }
            break;
          case 'children':
          case 'className':
            // Ignore
            break;
          case 'defaultValue':
            // Use default values if normal values are not present
            if (!props.value) {
              renderedString += ` value="${
                isString(value) ? escapeText(value) : value
              }"`;
            }
            break;
          case 'defaultChecked':
            // Use default values if normal values are not present
            if (!props.checked && value) {
              renderedString += ` checked="${value}"`;
            }
            break;
          default:
            if (isAttributeNameSafe(prop)) {
              if (isString(value)) {
                renderedString += ` ${prop}="${escapeText(value)}"`;
              } else if (isNumber(value)) {
                renderedString += ` ${prop}="${value}"`;
              } else if (value === true) {
                renderedString += ` ${prop}`;
              }
              break;
            }
        }
      }
    }

    renderedString += `>`;
    this.push(renderedString);

    if (String(type).match(/[\s\n/='"\0<>]/)) {
      throw renderedString;
    }

    if (isVoidElement) {
      return;
    } else {
      if (html) {
        this.push(html);
        this.push(`</${type}>`);
        return;
      }
    }
    const childFlags = vNode.childFlags;

    if (childFlags === ChildFlags.HasInvalidChildren) {
      this.push(`</${type}>`);
      return;
    }

    return Promise.resolve(
      this.renderChildren(vNode.children, context, childFlags),
    ).then(() => {
      this.push(`</${type}>`);
    });
  }
}

export function streamAsString(node) {
  return new RenderStream(node);
}
